﻿using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;

namespace OmniKits.Security.Cryptography
{
    using EssIL;

    using Core;
    using Extensions;

#pragma warning disable CS1591
    [Flags]
    public enum CrcPolynomialFlags
    {
        None = 0,
        Reversed = 1,
        IgnoreEdgeBits = 2,
        Reciprocal = 4,
        ReversedReciprocal = Reversed | Reciprocal,
        AllKnownFlags = Reversed | IgnoreEdgeBits | Reciprocal,
    }

    public struct CRC32Parameters
    {
        public int Polynomial;
        public int InitValue;
        public int FinalXor;
        public CrcPolynomialFlags Flags;

        public CRC32Parameters(int polynomial, int initValue = -1, int finalXor = -1, CrcPolynomialFlags flags = CrcPolynomialFlags.Reversed)
        {
            if (flags.TestFlag(~CrcPolynomialFlags.AllKnownFlags).GetValueOrDefault(true))
                throw new ArgumentException(null, nameof(flags));

            Polynomial = polynomial;
            InitValue = initValue;
            FinalXor = finalXor;
            Flags = flags;
        }
    }
#pragma warning restore CS1591

    public class CRC32 : HashAlgorithm
    {
#pragma warning disable 1591

        public const int DefaultInitValue = -1;
        public const int DefaultFinalXor = -1;
        public const int DefaultPolynomial = unchecked((int)0xEDB88320);
        public const int CastagnoliPolynomial = unchecked((int)0x82F63B78);

        private static readonly LazyDictionary<uint, uint[]> PrecomputedTables = new LazyDictionary<uint, uint[]>();

        public int Polynomial { get; }
        public int InitValue { get; }
        public int FinalXor { get; }

        private uint _CrcValue;
        private uint _InitValue;
        private uint _FinalXor;
        private uint _Polynomial;

        private uint[] _CRC32Table;

        private static uint[] MakeCRC32Table(uint polynomial)
        {
            var p = polynomial;
            var table = new uint[0x100];
            for (var i = 0; i < table.Length; i++)
            {
                var t = (uint)i;
                for (var j = 0; j < 8; j++)
                {
                    var f = (t & 1) == 0;
                    t >>= 1;
                    if (f) continue;
                    t ^= p;
                }
                table[i] = t;
            }
            return table;
        }

        private static uint[] GetCRC32Table(uint polynomial) => PrecomputedTables.GetOrAdd(polynomial, () => MakeCRC32Table(polynomial));

        public static bool IsPolynomialValid(int polynomial) => (polynomial & 1) == 1;
        public static bool IsReversedPolynomialValid(int polynomial) => polynomial < 0;

        private static void CheckPolynomial(int polynomial, string paramName)
        {
            if (IsReversedPolynomialValid(polynomial)) return;
            throw new ArgumentException(null, paramName);
        }

        public static int GetReversedPolynomial(int value) => value.FlipBits();
        const int HighestBit = unchecked((int)0x80000000u);
        public static int GetReversedReciprocalPolynomial(int value) => (value << 1 | (value >> 31 & 1));
        public static int GetReciprocalPolynomial(int value) => GetReversedPolynomial(GetReversedReciprocalPolynomial(value));
        public static int FromReversedReciprocalPolynomial(int value) => (value >> 1 & ~HighestBit | (value << 31 & HighestBit));

        private static void EnsureParameters(ref CRC32Parameters @params, string paramName)
        {
            if (@params.Flags.TestFlag(~CrcPolynomialFlags.AllKnownFlags).GetValueOrDefault(true))
                throw new ArgumentException(null, $"{paramName}.Flags");

            if (@params.Flags.Has(CrcPolynomialFlags.Reciprocal))
            {
                if (@params.Flags.Has(CrcPolynomialFlags.Reversed))
                {
                    if (!(@params.Flags.Has(CrcPolynomialFlags.IgnoreEdgeBits) || IsReversedPolynomialValid(@params.Polynomial)))
                        throw new ArgumentException(null, $"{paramName}.Polynomial");

                    @params.Polynomial = GetReciprocalPolynomial(@params.Polynomial);
                    @params.Flags ^= CrcPolynomialFlags.Reciprocal;
                }
                else
                {
                    if (!(@params.Flags.Has(CrcPolynomialFlags.IgnoreEdgeBits) || IsPolynomialValid(@params.Polynomial)))
                        throw new ArgumentException(null, $"{paramName}.Polynomial");

                    @params.Polynomial = FromReversedReciprocalPolynomial(@params.Polynomial);
                    @params.Flags ^= CrcPolynomialFlags.Reversed | CrcPolynomialFlags.Reciprocal;
                }
            }
            else
            {
                if (!@params.Flags.Has(CrcPolynomialFlags.Reversed))
                {
                    @params.Polynomial = GetReversedPolynomial(@params.Polynomial);
                    @params.Flags |= CrcPolynomialFlags.Reversed;
                }

                if (@params.Flags.Has(CrcPolynomialFlags.IgnoreEdgeBits))
                {
                    @params.Polynomial |= HighestBit;
                }
                else if (!IsReversedPolynomialValid(@params.Polynomial))
                {
                    throw new ArgumentException(null, $"{paramName}.Polynomial");
                }
            }
        }

        public static void TransformParameters(ref CRC32Parameters @params)
        {
            EnsureParameters(ref @params, nameof(@params));
        }
        public static CRC32Parameters TranslateParameters(CRC32Parameters @params)
        {
            EnsureParameters(ref @params, nameof(@params));
            return @params;
        }

        public static unsafe int[] GetPrecomputedCRC32Table(int polynomial)
        {
            CheckPolynomial(polynomial, nameof(polynomial));

            var table = GetCRC32Table((uint)polynomial);
            var len = table.Length;
            var result = new int[len];
            fixed (uint* p = &table[0])
            {
                Marshal.Copy((IntPtr)p, result, 0, len);
            }
            return result;
        }

        public CRC32()
            : this(DefaultPolynomial)
        { }

        public CRC32(int polynomial, int initValue = DefaultInitValue, int finalXor = DefaultFinalXor)
            : this(new CRC32Parameters(polynomial, initValue, finalXor))
        { }

        public CRC32(CRC32Parameters @params)
        {
            EnsureParameters(ref @params, nameof(@params));

            Polynomial = @params.Polynomial;
            InitValue = @params.InitValue;
            FinalXor = @params.FinalXor;

            _InitValue = (uint)@params.InitValue;
            _FinalXor = (uint)@params.FinalXor;
            _Polynomial = (uint)@params.Polynomial;
            _CRC32Table = GetCRC32Table(_Polynomial);

            Initialize();
        }

        public override void Initialize()
        {
            _CrcValue = _InitValue;
        }

        protected override void HashCore(byte[] array, int ibStart, int cbSize)
        {
            var end = ibStart + cbSize;
            for (int i = ibStart; i < end; i++)
                _CrcValue = (_CrcValue >> 8) ^ _CRC32Table[array[i] ^ _CrcValue & 0xFF];
        }

        protected override byte[] HashFinal()
        {
            var bytes = BitConverter.GetBytes(_CrcValue ^ _FinalXor);
            if (BitConverter.IsLittleEndian)
                Array.Reverse(bytes);
            return bytes;
        }
    }
}
